package chpn.id;

import java.time.LocalDateTime;
import java.util.Random;

/**
 * 获取64位整数的算法 ， 改进版雪花算法 (秒级)
 * ----
 * 优点：
 *    1. 支持百年期限
 *    2. 生成ID是有序的 趋势递增的
 *    3. 支持时间回拨
 *        时间正常回拨，不会发生冲突，
 *        如果同时有 workerId复用冲突， 出现重复ID的概率为 < ( 单表每秒获取ID个数的最大值/4194304 )
 *    4. 支持分布式部署
 *    5. id可以解码出时间
 *    6. 每秒最多4,194,304个ID
 *  ----
 * 分为三个段 ( 34 + 8 + 22 = 64 )
 * 34bit 为当前时间( 年份后三位 8b +  月 4b + 日 5b +  时 5b + 分 6b + 秒 6b )
 *          （00年 到 127年 正数 ，127年到 255年 负数）
 * 8bit  worker序号[0-255]
 * 22bit 循环自增序号
 *      [0 - 4,194,303]
 * 每次获取id 检查是否发生时间回拨，如果当前时间小于上次的时间，则更新worker序号
 * （ 2128 后是负数 ）
 */
public class SnowflakeSj {
    // worker ID (0-255)
    long workerId;

    // 上次生成ID的时间
    private LocalDateTime lastDateTime = null;

    // 序列号，初始为随机数
    private long sequence;
    
    // 同一秒内第一个序列号
    private long firstSequenceInSecond;

    // 各部分占用的位数
    private final long TIME_BITS = 34L;
    private final long WORKER_ID_BITS = 8L;
    private final long SEQUENCE_BITS = 22L;

    // 各部分的最大值
    private final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 255
    private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4,194,303

    // 各部分向左的位移
    private final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private final long TIME_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;

    // 时间各部分的位数
    private final int YEAR_BITS = 8;
    private final int MONTH_BITS = 4;
    private final int DAY_BITS = 5;
    private final int HOUR_BITS = 5;
    private final int MINUTE_BITS = 6;
    private final int SECOND_BITS = 6;

    // 时间各部分的最大值
    private final int MAX_YEAR = ~(-1 << YEAR_BITS); // 255
    private final int MAX_MONTH = ~(-1 << MONTH_BITS); // 15
    private final int MAX_DAY = ~(-1 << DAY_BITS); // 31
    private final int MAX_HOUR = ~(-1 << HOUR_BITS); // 31
    private final int MAX_MINUTE = ~(-1 << MINUTE_BITS); // 63
    private final int MAX_SECOND = ~(-1 << SECOND_BITS); // 63

    // 时间各部分的位移
    private final int SECOND_SHIFT = 0;
    private final int MINUTE_SHIFT = SECOND_BITS;
    private final int HOUR_SHIFT = MINUTE_SHIFT + MINUTE_BITS;
    private final int DAY_SHIFT = HOUR_SHIFT + HOUR_BITS;
    private final int MONTH_SHIFT = DAY_SHIFT + DAY_BITS;
    private final int YEAR_SHIFT = MONTH_SHIFT + MONTH_BITS;

    WorkerIdGenerator workerIdGenerator;

    public SnowflakeSj(WorkerIdGenerator workerIdGenerator) {
        this.workerIdGenerator = workerIdGenerator;
        this.workerId = workerIdGenerator.getNewWorkerId() & MAX_WORKER_ID;
        // 初始化序列号为随机数
        this.sequence = new Random().nextInt((int) MAX_SEQUENCE);
        // 初始化同一秒内第一个序列号
        this.firstSequenceInSecond = this.sequence;
    }

    public long getWorkerId() {
        return workerId;
    }

    /**
     * 获取一个新的ID
     */
    public synchronized long getNextId() {
        LocalDateTime currentDateTime = LocalDateTime.now();

        // 处理时间回拨问题
        if (lastDateTime != null && currentDateTime.isBefore(lastDateTime)) {
            // 如果发生时间回拨，更新机器序列号
            workerId = workerIdGenerator.getNewWorkerId() & MAX_WORKER_ID;
            // 重置序列号
            sequence = new Random().nextInt((int) MAX_SEQUENCE);
        }

        // 如果是同一秒内，则增加序列号
        if (lastDateTime != null && isSameSecond(currentDateTime, lastDateTime)) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            
            // 如果序列号回到了同一秒内第一个序列号，表示已经循环一圈，抛出异常
            if (sequence == firstSequenceInSecond) {
                throw new RuntimeException("序列号溢出");
            }
        } else {
            // 不同秒内，重置序列号为随机数
            sequence = new Random().nextInt((int) MAX_SEQUENCE);
            // 记录同一秒内第一个序列号
            firstSequenceInSecond = sequence;
        }

        // 更新上次生成ID的时间
        lastDateTime = currentDateTime;

        // 组装ID
        return (encodeTime(currentDateTime) << TIME_SHIFT) | // 时间部分
               (workerId << WORKER_ID_SHIFT) | // 机器ID部分
               sequence; // 序列号部分
    }

    /**
     * 将时间编码为34位整数
     * 年份后三位 8b +  月 4b + 日 5b +  时 5b + 分 + 6b + 秒 6b
     */
    private long encodeTime(LocalDateTime dateTime) {
        int year = dateTime.getYear() % 1000; // 取年份的后三位
        int month = dateTime.getMonthValue();
        int day = dateTime.getDayOfMonth();
        int hour = dateTime.getHour();
        int minute = dateTime.getMinute();
        int second = dateTime.getSecond();

        // 检查各部分是否超出最大值
        if (year > MAX_YEAR || month > MAX_MONTH || day > MAX_DAY ||
            hour > MAX_HOUR || minute > MAX_MINUTE || second > MAX_SECOND) {
            throw new IllegalArgumentException("Time value exceeds maximum allowed value");
        }

        // 组装时间编码
        return ((long) year << YEAR_SHIFT) |
               ((long) month << MONTH_SHIFT) |
               ((long) day << DAY_SHIFT) |
               ((long) hour << HOUR_SHIFT) |
               ((long) minute << MINUTE_SHIFT) |
               ((long) second << SECOND_SHIFT);
    }

    /**
     * 判断两个时间是否在同一秒
     */
    private boolean isSameSecond(LocalDateTime dt1, LocalDateTime dt2) {
        return dt1.getYear() == dt2.getYear() &&
               dt1.getMonthValue() == dt2.getMonthValue() &&
               dt1.getDayOfMonth() == dt2.getDayOfMonth() &&
               dt1.getHour() == dt2.getHour() &&
               dt1.getMinute() == dt2.getMinute() &&
               dt1.getSecond() == dt2.getSecond();
    }

    /**
     * 等待下一秒
     */
    private LocalDateTime waitNextSecond(LocalDateTime lastDateTime) {
        LocalDateTime currentDateTime;
        do {
            currentDateTime = LocalDateTime.now();
        } while (isSameSecond(currentDateTime, lastDateTime));
        return currentDateTime;
    }

    /**
     * 从ID中解码时间
     */
    public static LocalDateTime decodeTime(long id) {
        // 提取时间部分
        long timeCode = (id >> (8 + 22));

        // 解析各个时间组件
        int second = (int) (timeCode & 0x3F);
        int minute = (int) ((timeCode >> 6) & 0x3F);
        int hour = (int) ((timeCode >> 12) & 0x1F);
        int day = (int) ((timeCode >> 17) & 0x1F);
        int month = (int) ((timeCode >> 22) & 0xF);
        int year = (int) ((timeCode >> 26) & 0xFF) + 2000; // 加上1000得到完整年份

        return LocalDateTime.of(year, month, day, hour, minute, second);
    }
}
